import beautify from "js-beautify";
import fs from "fs-extra";
import path from "path";
import { minify, MinifyOptions } from "terser";
import _ from "lodash";

interface MangleNameCache {
    vars?: {
        props: { [raw: string]: string }
    },
    props?: {
        props: { [raw: string]: string }
    }
}

export class Mangler {
    private readonly alwaysReserved = [
        '__tgjsEvalScript',
        '__tgjsGetGenericMethod',
        '__tgjsGetLoader',
        '__tgjsGetNestedTypes',
        '__tgjsLoadType',
        '__tgjsRegisterTickHandler',
        '__tgjsSetPromiseRejectCallback',
        // Assets/Puerts_webgl/Runtime/Plugins/WebGL/puerts.jslib
        "SetCallV8",
        "GetLibVersion",
        "GetApiLevel",
        "GetLibBackend",
        "CreateJSEngine",
        "CreateJSEngineWithExternalEnv",
        "DestroyJSEngine",
        "SetGlobalFunction",
        "GetLastExceptionInfo",
        "LowMemoryNotification",
        "IdleNotificationDeadline",
        "RequestMinorGarbageCollectionForTesting",
        "RequestFullGarbageCollectionForTesting",
        "SetGeneralDestructor",
        "SetModuleResolver",
        "ExecuteModule",
        "Eval",
        "_RegisterClass",
        "RegisterStruct",
        "RegisterFunction",
        "RegisterProperty",
        "ReturnClass",
        "ReturnObject",
        "ReturnNumber",
        "ReturnString",
        "ReturnBigInt",
        "ReturnBoolean",
        "ReturnDate",
        "ReturnNull",
        "ReturnFunction",
        "ReturnJSObject",
        "ReturnArrayBuffer",
        "ReturnCSharpFunctionCallback",
        "GetArgumentType",
    //    "GetArgumentValue",
        "GetJsValueType",
        "GetTypeIdFromValue",
        "GetNumberFromValue",
        "GetDateFromValue",
        "GetStringFromValue",
        "GetBooleanFromValue",
        "ValueIsBigInt",
        "GetBigIntFromValue",
        "GetObjectFromValue",
        "GetFunctionFromValue",
        "GetJSObjectFromValue",
        "GetArrayBufferFromValue",
        "SetNumberToOutValue",
        "SetDateToOutValue",
        "SetStringToOutValue",
        "SetBooleanToOutValue",
        "SetBigIntToOutValue",
        "SetObjectToOutValue",
        "SetNullToOutValue",
        "SetArrayBufferToOutValue",
        "ThrowException",
        "PushNullForJSFunction",
        "PushDateForJSFunction",
        "PushBooleanForJSFunction",
        "PushBigIntForJSFunction",
        "PushStringForJSFunction",
        "__PushStringForJSFunction",
        "PushNumberForJSFunction",
        "PushObjectForJSFunction",
        "PushJSFunctionForJSFunction",
        "PushJSObjectForJSFunction",
        "PushArrayBufferForJSFunction",
        "SetPushJSFunctionArgumentsCallback",
        "InvokeJSFunction",
        "GetFunctionLastExceptionInfo",
        "ReleaseJSFunction",
        "ReleaseJSObject",
        "GetResultType",
        "GetNumberFromResult",
        "GetDateFromResult",
        "GetStringFromResult",
        "GetBooleanFromResult",
        "ResultIsBigInt",
        "GetBigIntFromResult",
        "GetObjectFromResult",
        "GetTypeIdFromResult",
        "GetFunctionFromResult",
        "GetJSObjectFromResult",
        "GetArrayBufferFromResult",
        "ResetResult",
        "ClearModuleCache",
        "CreateInspector",
        "DestroyInspector",
        "InspectorTick",
        "LogicTick",
        "SetLogCallback",
        // serverlist.php
        'ver', 'name', 'id', 'groups', 'servers'
    ];

    private reserved!: string[];
    private nameCache!: MangleNameCache;
    private minifyOption!: MinifyOptions;

    public async beginMangle(reserved: string[], nameCacheFile: string, keepClassNames: boolean, mangleProperties: boolean): Promise<void> {
        this.reserved = _.union(this.alwaysReserved, reserved);
        if (fs.existsSync(nameCacheFile)) {
            this.nameCache = await fs.readJson(nameCacheFile);
        } else {
            this.nameCache = {};
        }
        this.minifyOption = {
            sourceMap: true,
            mangle: {
                reserved: this.reserved,
                keep_classnames: keepClassNames,
                properties: mangleProperties ? { reserved: this.reserved, keep_quoted: true } : false
            },
            nameCache: this.nameCache
        };
    }

    public async endMangle(nameCacheFile: string): Promise<void> {
        await fs.writeJSON(nameCacheFile, this.nameCache, { spaces: 2 });
    }

    public async mangleAll(inputDir: string, outputDir: string, includeExt: '.js' | '.json'): Promise<void> {
        const files = await fs.readdir(inputDir);
        for (const f of files) {
            const inputFile = path.join(inputDir, f);
            const outputFile = path.join(outputDir, f);
            const fstat = await fs.stat(inputFile);
            if (fstat.isDirectory()) {
                await fs.ensureDir(outputFile);
                await this.mangleAll(inputFile, outputFile, includeExt);
            } else {
                const ext = path.extname(f);
                if (ext === includeExt) {
                    if (ext === '.js') {
                        await this.mangleOneJs(inputFile, outputFile);
                    } else if (ext === '.json') {
                        await this.mangleOneJson(inputFile, outputFile);
                    }
                }
            }
        }
    }

    public async mangleOneJs(inputFile: string, outputFile: string): Promise<void> {
        // console.log('mangling:', inputFile);
        if (inputFile.includes('node_modules')) return;
        const content = await fs.readFile(inputFile, 'utf-8');
        const result = await minify(content, this.minifyOption);
        // const result = await minify(content, { sourceMap: true, module: true, keep_classnames: false, keep_fnames: false, toplevel: true, compress: { defaults: true, reduce_vars: true }, mangle: { reserved: this.reserved, properties: { regex: /(?<!globalThis\.)./, reserved: this.reserved } }, nameCache: this.nameCache });        
        // let newContent = `console.log("${relative} start...");\n` + result.code + `\nconsole.log("${relative} end...");`;
        let newContent = result.code!;
        newContent = beautify.js_beautify(newContent, { indent_size: 2, space_in_empty_paren: true });
        await fs.writeFile(outputFile, newContent, 'utf-8');
    }

    public async mangleOneJson(inputFile: string, outputFile: string): Promise<void> {
        // console.log('mangling:', inputFile);
        const content = await fs.readFile(inputFile, 'utf-8');
        var result = await minify(content, { compress: false, mangle: { properties: true, reserved: this.reserved }, nameCache: this.nameCache });
        let o;
        const obj = eval('o=' + result.code!);
        await fs.writeJSON(outputFile, obj);
    }

    public async patchJs(dir: string): Promise<void> {
        // 对uts和delegate打补丁
        const delegateStr = this.nameCache.props?.props['$delegate'];
        const utsStr = this.nameCache.vars?.props['$uts'];
        console.log(`[Mangler] patch delegate to ${delegateStr}, patch uts to ${utsStr}`);
        await this.patchAllJsInternal(dir, delegateStr, utsStr);
    }

    private async patchAllJsInternal(input: string, delegateStr: string | undefined, utsStr: string | undefined): Promise<void> {
        if (delegateStr != null) {
            const fstat = await fs.stat(input);
            if (fstat.isDirectory()) {
                const files = await fs.readdir(input);
                for (const f of files) {
                    const file = path.join(input, f);
                    await this.patchAllJsInternal(file, delegateStr, utsStr);
                }
            } else {
                const ext = path.extname(input);
                if (ext === '.js') {
                    const content = await fs.readFile(input, 'utf-8');
                    let newContent = content.replaceAll('delegate', delegateStr);
                    if (utsStr != null) {
                        newContent = newContent.replace('globalThis.uts', `globalThis.${utsStr}`);
                    }
                    await fs.writeFile(input, newContent, 'utf-8');
                }
            }
        }        
    }
}
